# encoding: utf-8

import woo.config
if 'qt5' in woo.config.features:
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    from PyQt5 import QtGui

from minieigen import *
# don't import * from woo, it would be circular import
from woo.core import Object

import re,itertools,math
import woo.core, woo.utils
log=woo.utils.makeLog('ObjectEditor')
import woo.qt
import woo.document
import sys
import os.path, os



from .ExceptionDialog import *

seqObjectShowType=True # show type headings in serializable sequences (takes vertical space, but makes the type hyperlinked)

# BUG: cursor is moved to the beginnign of the input field even if it has focus
#
# checking for focus seems to return True always and cursor is never moved
#
# the 'True or' part effectively disables the condition (so that the cursor is moved always), but it might be fixed in the future somehow
#
# if True or w.hasFocus(): w.home(False)
#
#

colormapIconSize=(50,20)

def _ensureUnicode(s): return s if isinstance(s,str) else s.decode('utf-8')


def makeColormapIcons():
    ret=[]
    for cmap in range(len(woo.master.cmaps)):
        wd,ht=colormapIconSize
        img=QtGui.QImage(wd,ht,QtGui.QImage.Format_RGB32)
        for col in range(wd):
            c=255*woo.utils.mapColor(col*1./wd,cmap=cmap)
            #c=(0,0,0)
            cc=QtGui.qRgb(int(c[0]),int(c[1]),int(c[2]))
            for row in range(ht):
                img.setPixel(col,row,cc)
        ret.append(QtGui.QIcon(QtGui.QPixmap.fromImage(img)))
    return ret

# construct colormap icons, for later use
# use this proxy function so that makeColormapIcons is not called at import time
# since that leads to crash (qt is not fully initialized yet)
_colormapIcons=[]
def getColormapIcons():
    global _colormapIcons
    if not _colormapIcons: _colormapIcons=makeColormapIcons()
    return _colormapIcons


class WidgetUpdatesDisabled():
    'Context manager/decorator for disabling updates of qt widgets temporarily'
    def __init__(self,widget): self.widget=widget
    def __enter__(self): self.widget.setUpdatesEnabled(False)
    def __exit__(self,eType,eValue,eTrace): self.widget.setUpdatesEnabled(True)



# HACK: extend the QLineEdit class
# set text but preserve cursor position
def QLineEdit_setTextStable(self,text):
    c=self.cursorPosition(); self.setText(text); self.setCursorPosition(c)
def QSpinBox_setValueStable(self,value):
    c=self.lineEdit().cursorPosition(); self.setValue(value); self.lineEdit().setCursorPosition(c)
QLineEdit.setTextStable=QLineEdit_setTextStable
QSpinBox.setValueStable=QSpinBox_setValueStable

class AttrEditor():
    """Abstract base class handing some aspects common to all attribute editors.
    Holds exacly one attribute which is updated whenever it changes."""
    def __init__(self,getter=None,setter=None):
        self.getter,self.setter=getter,setter
        self.hot=False
        self.multiplier=None
        self.widget=None
        self.readonly=False
    def refresh(self): pass
    def update(self): pass
    def isHot(self,hot=True):
        "Called when the widget gets focus; mark it hot, change colors etc."
        if hot==self.hot: return
        self.hot=hot
        if hot: self.setStyleSheet('QWidget { background: red }')
        else: self.setStyleSheet('QWidget { background: none }')
    def sizeHint(self): return QSize(100,12)
    def trySetter(self,val):
        if not self.readonly:
            try: self.setter(val)
            except BaseException as e:
                log.warning(f'Error setting value {val}: {type(e)} {e}')
                # self.setEnabled(False)
                # log.exception(f'Error setting value {val}:')
        # self.isHot(False)
    def multiplierChanged(self,convSpec):
        raise RuntimeError("This widget %s has no multiplierChanged method defined."%self.__class__.__name__)
    def focusInEvent(self,event):
        # log.info(f'AttrEditor.focusInEvent {self}')
        self.doFocusIn()
        super().focusInEvent(event)
    def focusOutEvent(self,event):
        # log.info(f'AttrEditor.focusOutEvent {self}')
        self.doFocusOut()
        super().focusOutEvent(event)
    def doFocusIn(self):
        # log.info(f'AttrEditor.doFocusIn {self}')
        self.refresh()
        self.isHot(True)
    def doFocusOut(self):
        # log.info(f'AttrEditor.doFocusOut {self}')
        self.update()
        self.isHot(False)
        #super().focusOutEvent(event)

class AttrEditor_Bool(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        self.checkBox=QCheckBox(self)
        lay=QVBoxLayout(self); lay.setSpacing(0); lay.setContentsMargins(0,0,0,0); lay.addStretch(1); lay.addWidget(self.checkBox); lay.addStretch(1)
        self.checkBox.clicked.connect(self.update)
    def refresh(self):
        assert(not self.multiplier)
        self.checkBox.setChecked(self.getter())
    def update(self): self.trySetter(self.checkBox.isChecked())

class AttrEditor_Int(AttrEditor,QSpinBox):
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QSpinBox.__init__(self,parent)
        self.setRange(int(-1e9),int(1e9)); self.setSingleStep(1);
        self.valueChanged.connect(self.update)
    def refresh(self):
        assert(not self.multiplier)
        self.setValueStable(self.getter())
    def update(self):  self.trySetter(self.value())

class AttrEditor_Str(AttrEditor,QLineEdit):
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QLineEdit.__init__(self,parent)
        # self.textEdited.connect(self.isHot)
        #self.selectionChanged.connect(self.isHot)
        #self.editingFinished.connect(self.update)
    def refresh(self):
        assert(not self.multiplier)
        self.setTextStable(self.getter())
    def update(self):  self.trySetter(str(self.text()))

class AttrEditor_Float(AttrEditor,QLineEdit):
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QLineEdit.__init__(self,parent)
        #self.textEdited.connect(self.isHot)
        #self.selectionChanged.connect(self.isHot)
        #self.editingFinished.connect(self.update)
    def refresh(self):
        v=self.getter()
        if self.multiplier: v*=self.multiplier
        self.setTextStable(str(v))
    def update(self):
        # when everything is deleted, don't refresh because of float('') raising ValueError
        if self.hot and not self.text(): return
        try:
            v=float(self.text())
            if self.multiplier: v/=self.multiplier
            self.trySetter(v)
        except ValueError: self.refresh()
    def multiplierChanged(self,convSpec):
        if isinstance(self.multiplier,tuple): raise RuntimeError("Float cannot have multiple units.")
        if self.multiplier: self.setToolTip(u'Converting %s (× %g)'%(convSpec,self.multiplier))
        else: self.setToolTip('')
        self.refresh()

class AttrEditor_Quaternion(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        self.grid=QHBoxLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        for i in range(4):
            if i==3:
                f=QFrame(self); f.setFrameShape(QFrame.VLine); f.setFrameShadow(QFrame.Sunken); f.setFixedWidth(4) # add vertical divider (axis | angle)
                self.grid.addWidget(f)
            w=QLineEdit('')
            self.grid.addWidget(w);
            w.textEdited.connect(self.isHot)
            w.selectionChanged.connect(self.isHot)
            w.editingFinished.connect(self.update)
    def refresh(self):
        assert(not self.multiplier)
        val=self.getter(); axis,angle=val.toAxisAngle()
        for i in (0,1,2,4):
            w=self.grid.itemAt(i).widget(); w.setTextStable(str(axis[i] if i<3 else angle));
            #if True or not w.hasFocus(): w.home(False)
    def update(self):
        try:
            x=[float((self.grid.itemAt(i).widget().text())) for i in (0,1,2,4)]
        except ValueError: self.refresh()
        q=Quaternion(Vector3(x[0],x[1],x[2]),x[3]); q.normalize() # from axis-angle
        self.trySetter(q) 
    def setFocus(self): self.grid.itemAt(0).widget().setFocus()


class AttrEditor_IntRange(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter): 
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        curr,(mn,mx)=getter()
        self.slider,self.spin=QSlider(self),QSpinBox(self)
        self.grid.addWidget(self.spin,0,0)
        self.grid.addWidget(self.slider,0,1,1,2) # rowSpan=1,columnSpan=2
        self.slider.setMinimum(mn); self.slider.setMaximum(mx)
        self.spin.setMinimum(mn); self.spin.setMaximum(mx)
        self.slider.setOrientation(Qt.Horizontal)
        self.slider.setFocusPolicy(Qt.ClickFocus)
        self.spin.valueChanged.connect(self.updateFromSpin)
        self.slider.sliderMoved.connect(self.sliderMoved)
        self.slider.sliderReleased.connect(self.updateFromSlider)
    def refresh(self):
        assert(not self.multiplier)
        curr,(mn,mx)=self.getter()
        c=self.spin.lineEdit().cursorPosition()
        self.spin.setValueStable(curr); self.spin.setMinimum(mn); self.spin.setMaximum(mx)
        self.spin.lineEdit().setCursorPosition(c)
        self.slider.setValue(curr)
    def updateFromSpin(self):
        self.slider.setValue(self.spin.value())
        self.trySetter(self.slider.value())
    def updateFromSlider(self):
        self.spin.setValue(self.slider.value())
        self.trySetter(self.slider.value())
    def sliderMoved(self,val):
        self.isHot(True)
        self.spin.setValue(val) # self.slider.value())
    def setFocus(self): self.slider.setFocus()


class AttrEditor_FloatRange(AttrEditor,QFrame):
    sliDiv=500
    def __init__(self,parent,getter,setter): 
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        curr,(mn,mx)=self.multipliedGetter()
        self.slider,self.edit=QSlider(self),QLineEdit(str(curr))
        self.grid=QHBoxLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        self.grid.addWidget(self.edit); self.grid.addWidget(self.slider)
        self.grid.setStretchFactor(self.edit,1); self.grid.setStretchFactor(self.slider,2)
        self.slider.setMinimum(0); self.slider.setMaximum(self.sliDiv)
        self.slider.setOrientation(Qt.Horizontal)
        self.slider.setFocusPolicy(Qt.ClickFocus)
        self.edit.textEdited.connect(self.isHot)
        self.edit.selectionChanged.connect(self.isHot)
        self.edit.editingFinished.connect(self.updateFromText)
        self.slider.sliderMoved.connect(self.sliderMoved) #
        self.slider.sliderReleased.connect(self.sliderReleased) #
    def multipliedGetter(self):
        curr,(mn,mx)=self.getter()
        if self.multiplier:
            curr*=self.multiplier; mn*=self.multiplier; mx*=self.multiplier
        return curr,(mn,mx)
    def refresh(self):
        curr,(mn,mx)=self.multipliedGetter()
        self.edit.setTextStable('%g'%curr)
        if not math.isnan(curr):
            self.slider.setValue(int(self.sliDiv*((1.*curr-mn)/(1.*mx-mn))))
            self.slider.setEnabled(True)
        else: self.slider.setEnabled(False)
    def updateFromText(self):
        curr,(mn,mx)=self.multipliedGetter()
        try:
            value=float(self.edit.text())
            self.slider.setValue(int(self.sliDiv*((1.*value-mn)/(1.*mx-mn))))
            self.trySetter(value)
        except ValueError: self.refresh()
    def sliderPosToNum(self,sliderValue):
        curr,(mn,mx)=self.getter()
        return mn+sliderValue*(1./self.sliDiv)*(mx-mn)
    def sliderMoved(self,newSliderPos):
        self.isHot(True)
        self.edit.setText('%g'%self.sliderPosToNum(newSliderPos))
    def sliderReleased(self):
        value=self.sliderPosToNum(self.slider.value())
        self.edit.setText('%g'%value)
        if self.multiplier: value/=self.multiplier
        self.trySetter(value)
    def setFocus(self): self.slider.setFocus()
    def multiplierChanged(self,convSpec):
        if isinstance(self.multiplier,tuple): raise RuntimeError("Float range cannot have multiple units.")
        log.debug("New multiplier is "+str(self.multiplier))
        if self.multiplier: self.setToolTip("Unit-conversion %s: factor %g"%(convSpec,self.multiplier))
        else: self.setToolTip('')
        self.refresh()
        #pass # this is OK

class AttrEditor_Choice(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter,isColormap):
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        curr,choices=getter()
        self.choices=choices
        if len(choices)<1: raise RuntimeError("There are 0 choices for this attribute?")
        self.combo=QComboBox(self)
        # if choices are single items, then we choose from those values
        # otherwise the first item is code value and second is the displayed value
        self.justValues=(choices[0].__class__!=tuple)
        self.admitValues=[(c if self.justValues else c[0]) for c in choices]
        if choices[0].__class__==tuple and len(choices[0])!=2: raise ValueError("Choice must be either single items or 2-tuples of code-value,display-value")
        for c in choices:
            self.combo.addItem(str(c if self.justValues else c[1])) 
        ### hack for COLORMAPS!!, only based on length of the choice list
        if isColormap:
            nCmaps=len(woo.master.cmaps)
            assert len(choices) in (nCmaps,nCmaps+1), "Number of colormap choices not equal to the number of known colormaps?" # +1 for default
            if len(choices)==nCmaps+1:
                for i in range(len(choices)): self.combo.setItemIcon(i,getColormapIcons()[(i-1) if i>0 else woo.master.cmap[0]])
            else:
                for i in range(len(choices)): self.combo.setItemIcon(i,getColormapIcons()[i])
            self.combo.setIconSize(QSize(colormapIconSize[0],colormapIconSize[1]))
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        self.grid.addWidget(self.combo,0,0)
        self.combo.activated.connect(self.update)
    def refresh(self):
        curr,choices=self.getter()
        # print curr,choices
        #if self.justValues:
        #else:
        if type(curr)==int:
            if curr<0 or curr>len(self.admitValues): raise ValueError("Current value out of range?")
            if curr!=self.combo.currentIndex(): self.combo.setCurrentIndex(curr)
        else:
            if curr not in self.admitValues:
                raise ValueError("Choice attribute value "+str(curr)+" is not within admitted values "+str(self.admitValues))
            if self.admitValues[self.combo.currentIndex()]!=curr: # update the combo
                self.combo.setCurrentIndex(self.admitValues.index(curr))

    def update(self):
        choice=self.choices[self.combo.currentIndex()]
        val=choice if self.justValues else choice[0]
        self.trySetter(val)
    def setFocus(self): self.combo.setFocus()
    def multiplierChanged(self,convSpec):
        if isinstance(self.multiplier,tuple): raise RuntimeError("Float choice cannot have multiple units.")
        else: self.setToolTip()
        for i,c in enumerate(self.choices):
            val=c if self.justValues else c[1]
            if c.__class__!=float: raise RuntimeError("Only float choices can meaningfully support unit multipliers")
            if self.multiplier: val*=self.multiplier
            self.combo.setItemText(i,str(val))
        self.refresh()

class AttrEditor_Bits(AttrEditor,QFrame):
    checkersPerRow=3
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        curr,self.bits=getter()
        if len(self.bits)<1: raise RuntimeError("There are 0 bits for this attribute?")
        self.checkers=[]
        self.value=0 # current value of checkboxes
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        for i,b in enumerate(self.bits):
            w=QCheckBox(self)
            w.setText(b)
            w.clicked.connect(self.update)
            self.checkers.append(w)
            self.grid.addWidget(w,i//self.checkersPerRow,i%self.checkersPerRow)
    def refresh(self):
        curr,bits=self.getter()
        if curr==self.value: return
        if curr>=2**len(self.checkers): raise RuntimeError("Value %d exceeds maximum value %d (=2^%d-1) representable by the UI bitfield"%(curr,2**len(self.checkers)-1,len(self.checkers))) 
        self.value=curr
        for bit,w in enumerate(self.checkers):
            b=curr&(1<<bit)
            if b!=w.isChecked(): w.setChecked(b)
    def update(self):
        val=0
        for bit,w in enumerate(self.checkers):
            if w.isChecked(): val|=(1<<bit)
        self.trySetter(val)
    def setFocus(self): self.checkers[0].setFocus()

class AttrEditor_RgbColor(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        self.butt=QPushButton()
        self.butt.clicked.connect(self.dialogShow)
        self.checkBox=QCheckBox('',self)
        self.checkBox.setStyleSheet('padding: 0px;')
        self.checkBox.toggled.connect(self.toggled)
        self.grid.addWidget(self.checkBox,0,0)
        self.grid.addWidget(self.butt,0,1)
        self.rgbWidgets=[]
        self.prevR=0
        self.dialog=None
        for i in (0,1,2):
            w=QLineEdit('')
            self.grid.addWidget(w,0,i+2)
            w.textEdited.connect(self.isHot)
            w.selectionChanged.connect(self.isHot)
            w.editingFinished.connect(self.update)
            self.rgbWidgets.append(w)
    def toggled(self,enabled):
        rgb=self.getter()
        if enabled:
            for i in 0,1,2:
                if math.isnan(rgb[i]): rgb[i]=self.prevR
        else:
            rgb[0]=float('nan')
        for w in self.rgbWidgets+[self.butt]: w.setEnabled(enabled)
    def to256c(self,f):
        return min(255,max(0,int(f*256))) if not math.isnan(f) else 0
    def to256(self,v): return (self.to256c(v[0]),self.to256c(v[1]),self.to256c(v[2]))
    def to256str(self,v,sep=','): return sep.join([str(self.to256c(v[0])),str(self.to256c(v[1])),str(self.to256c(v[2]))])
    def dialogShow(self):
        rgb=self.getter()
        self.dialog=QColorDialog(self)
        self.dialog.setCurrentColor(QColor(*self.to256(rgb)))
        self.dialog.setOptions(QColorDialog.NoButtons)
        self.dialog.currentColorChanged.connect(self.dialogChanged)
        self.dialog.show()
    def dialogChanged(self):
        rgba=self.dialog.currentColor().getRgb()
        self.setter(Vector3(*rgba[0:3])/256)
        self.refresh()
    def refresh(self):
        rgb=self.getter()
        for i in (0,1,2):
            self.rgbWidgets[i].setTextStable(str(rgb[i]))
        if not math.isnan(rgb[0]): self.prevR=rgb[0]
        self.butt.setStyleSheet('QPushButton { background-color: rgb(%s) }'%(self.to256str(rgb)))
        if sum([math.isnan(rgb[i]) for i in (0,1,2)]):
            if self.checkBox.isChecked(): self.checkBox.setChecked(False)
        else:
            if not self.checkBox.isChecked(): self.checkBox.setChecked(True)
    def update(self):
        try:
            rgb=Vector3([float(self.rgbWidgets[i].text()) for i in (0,1,2)])
        except ValueError: self.refresh()
        self.trySetter(rgb)

class AttrEditor_FileDir(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter,isDir,isExisting):
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        self.isDir,self.isExisting=isDir,isExisting
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        w=self.nameEdit=QLineEdit('')
        self.grid.addWidget(w,0,0)
        w.textEdited.connect(self.isHot)
        w.selectionChanged.connect(self.isHot)
        w.editingFinished.connect(self.update)
        b=self.butt=QPushButton()
        style=QApplication.style()
        if isDir: b.setIcon(style.standardIcon(QStyle.SP_DirIcon))
        else: b.setIcon(style.standardIcon(QStyle.SP_FileIcon))
        self.butt.clicked.connect(self.dialogShow)
        self.grid.addWidget(self.butt,0,2)
        rel=self.rel=QPushButton()
        rel.setCheckable(True)
        rel.setText('rel')
        rel.setToolTip(u'Toggle absolute/relative path.\nPaths are relative to the current directory,\nwhich is now %s.'%(str(os.getcwd())))
        rel.toggled.connect(self.relToggled)
        rel.setStyleSheet('padding: 0px;')
        self.grid.addWidget(rel,0,1)
        
    def dialogShow(self):
        curr=self.nameEdit.text()
        if self.isDir:    f=QFileDialog.getExistingDirectory(self,'Select directory',curr)
        elif self.isExisting: f=QFileDialog.getOpenFileName(self,'Select existing file',curr)
        else: f=QFileDialog.getSaveFileName(self,'Select file name',curr,options=QFileDialog.DontConfirmOverwrite)
        if isinstance(f,tuple): f=f[0]
        if not f: return # cancelled
        f=str(f)
        if self.rel.isChecked(): f=os.path.relpath(f)
        self.setter(f)
    def refresh(self):
        f=self.getter()
        self.nameEdit.setTextStable(f)
        self.rel.setChecked(not os.path.isabs(f))
    def update(self):
        f=str(self.nameEdit.text())
        self.trySetter(f)
        self.rel.setChecked(not os.path.isabs(f))
    def relToggled(self,isRel):
        f=str(self.nameEdit.text())
        if f=='': return # do nothing for empty path
        if isRel: self.trySetter(os.path.relpath(f))
        else: self.trySetter(os.path.abspath(f))
        
    

class AttrEditor_Se3(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter):
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        for row,col in itertools.product(range(2),range(5)): # one additional column for vertical line in quaternion
            if (row,col)==(0,3): continue
            if (row,col)==(0,4): self.grid.addWidget(QLabel(u'←<i>pos</i> ↙<i>ori</i>',self),row,col); continue
            if (row,col)==(1,3):
                f=QFrame(self); f.setFrameShape(QFrame.VLine); f.setFrameShadow(QFrame.Sunken); f.setFixedWidth(4); self.grid.addWidget(f,row,col); continue
            w=QLineEdit('')
            self.grid.addWidget(w,row,col);
            w.textEdited.connect(self.isHot)
            w.selectionChanged.connect(self.isHot)
            w.editingFinished.connect(self.update)
    def refresh(self):
        pos,ori=self.getter(); axis,angle=ori.toAxisAngle()
        for i in (0,1,2,4):
            w=self.grid.itemAtPosition(1,i).widget(); w.setTextStable(str(axis[i] if i<3 else angle));
            #if True or not w.hasFocus(): w.home(False)
        for i in (0,1,2):
            w=self.grid.itemAtPosition(0,i).widget(); w.setTextStable(str(pos[i]));
            #if True or not w.hasFocus(): w.home(False)
    def update(self):
        try:
            q=[float((self.grid.itemAtPosition(1,i).widget().text())) for i in (0,1,2,4)]
            v=[float((self.grid.itemAtPosition(0,i).widget().text())) for i in (0,1,2)]
        except ValueError: self.refresh()
        qq=Quaternion(Vector3(q[0],q[1],q[2]),q[3]); qq.normalize() # from axis-angle
        self.trySetter((v,qq)) 
    def setFocus(self): self.grid.itemAtPosition(0,0).widget().setFocus()


class QLineEdit_subattr(QLineEdit):
    def __init__(self,parent):
        if not isinstance(parent,AttrEditor): raise TypeError(f"Parent of QLineEdit_subattr must be AttrEditor (not {type(parent).__qualname__}).")
        super().__init__('',parent=parent)
        self._parent=parent
        self.returnPressed.connect(parent.update)
    def focusInEvent(self,event):
        log.warning('QLineEdit_subattr.focusInEvent')
        self._parent.doFocusIn()
        QLineEdit.focusInEvent(self,event) # show cursors etc
    def focusOutEvent(self,event):
        log.warning('QLineEdit_subattr.focusOutEvent')
        self._parent.doFocusOut()
        QLineEdit.focusOutEvent(self,event) # hides cursor etc


class AttrEditor_MatrixX(AttrEditor,QWidget):
    def __init__(self,parent,getter,setter,rows,cols,idxConverter):
        'idxConverter converts row,col tuple to either (row,col), (col) etc depending on what access is used for []'
        AttrEditor.__init__(self,getter,setter)
        QWidget.__init__(self,parent)
        self.rows,self.cols=rows,cols
        self.idxConverter=idxConverter
        self.setContentsMargins(0,0,0,0)
        val=self.getter()
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        for row,col in itertools.product(range(self.rows),range(self.cols)):
            w=QLineEdit_subattr(parent=self)
            self.grid.addWidget(w,row,col)
    def refresh(self):
        val=self.getter()
        for row,col in itertools.product(range(self.rows),range(self.cols)):
            w=self.grid.itemAtPosition(row,col).widget()
            v=val[self.idxConverter(row,col)]
            mult=(self.multiplier[col] if isinstance(self.multiplier,tuple) else self.multiplier)
            if mult!=None: v*=mult
            w.setTextStable(str(v))
            #if True or not w.hasFocus: w.home(False) # make the left-most part visible, if the text is wider than the widget
    def update(self):
        try:
            val=self.getter()
            for row,col in itertools.product(range(self.rows),range(self.cols)):
                w=self.grid.itemAtPosition(row,col).widget()
                if w.isModified():
                    v=float(w.text())
                    mult=(self.multiplier[col] if isinstance(self.multiplier,tuple) else self.multiplier)
                    if mult: v/=mult
                    val[self.idxConverter(row,col)]=v
            log.debug('setting'+str(val))
            self.trySetter(val)
        except ValueError: self.refresh()
    def setFocus(self): self.grid.itemAtPosition(0,0).widget().setFocus()
    def multiplierChanged(self,convSpec):
        if self.multiplier: self.setToolTip(convSpec)
        else: self.setToolTip('')
        log.debug("Multiplier changed to "+str(self.multiplier))
        self.refresh()

class AttrEditor_MatrixXi(AttrEditor,QFrame):
    def __init__(self,parent,getter,setter,rows,cols,idxConverter):
        'idxConverter converts row,col tuple to either (row,col), (col) etc depending on what access is used for []'
        AttrEditor.__init__(self,getter,setter)
        QFrame.__init__(self,parent)
        self.rows,self.cols=rows,cols
        self.idxConverter=idxConverter
        self.setContentsMargins(0,0,0,0)
        self.grid=QGridLayout(self); self.grid.setSpacing(0); self.grid.setContentsMargins(0,0,0,0)
        for row,col in itertools.product(range(self.rows),range(self.cols)):
            w=QSpinBox()
            w.setRange(int(-1e9),int(1e9)); w.setSingleStep(1);
            self.grid.addWidget(w,row,col);
        self.refresh() # refresh before connecting signals!
        for row,col in itertools.product(range(self.rows),range(self.cols)):
            self.grid.itemAtPosition(row,col).widget().valueChanged.connect(self.update)
    def refresh(self):
        val=self.getter()
        for row,col in itertools.product(range(self.rows),range(self.cols)):
            w=self.grid.itemAtPosition(row,col).widget().setValueStable(val[self.idxConverter(row,col)])
    def update(self):
        val=self.getter(); modified=False
        for row,col in itertools.product(range(self.rows),range(self.cols)):
            w=self.grid.itemAtPosition(row,col).widget()
            if w.value()!=val[self.idxConverter(row,col)]:
                modified=True; val[self.idxConverter(row,col)]=w.value()
        if not modified: return
        log.debug('setting'+str(val))
        self.trySetter(val)
    def setFocus(self): self.grid.itemAtPosition(0,0).widget().setFocus()

class AttrEditor_Vector6i(AttrEditor_MatrixXi):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixXi.__init__(self,parent,getter,setter,1,6,lambda r,c:c)
class AttrEditor_Vector3i(AttrEditor_MatrixXi):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixXi.__init__(self,parent,getter,setter,1,3,lambda r,c:c)
class AttrEditor_Vector2i(AttrEditor_MatrixXi):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixXi.__init__(self,parent,getter,setter,1,2,lambda r,c:c)

class AttrEditor_VectorX(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        val=getter()
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,len(val),lambda r,c:c)
class AttrEditor_Vector6(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,6,lambda r,c:c)
class AttrEditor_Vector3(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,3,lambda r,c:c)
class AttrEditor_Vector2(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,1,2,lambda r,c:c)
class AttrEditor_Matrix3(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,3,3,lambda r,c:(r,c))
class AttrEditor_MatrixXX(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        val=getter()
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,val.rows(),val.cols(),lambda r,c:(r,c))
class AttrEditor_AlignedBox3(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,2,3,lambda r,c:(r,c))
class AttrEditor_AlignedBox2(AttrEditor_MatrixX):
    def __init__(self,parent,getter,setter):
        AttrEditor_MatrixX.__init__(self,parent,getter,setter,2,2,lambda r,c:(r,c))

class Se3FakeType: pass

_fundamentalEditorMap={bool:AttrEditor_Bool,str:AttrEditor_Str,int:AttrEditor_Int,float:AttrEditor_Float,Quaternion:AttrEditor_Quaternion,Vector2:AttrEditor_Vector2,Vector3:AttrEditor_Vector3,Vector6:AttrEditor_Vector6,Matrix3:AttrEditor_Matrix3,Vector6i:AttrEditor_Vector6i,Vector3i:AttrEditor_Vector3i,Vector2i:AttrEditor_Vector2i,MatrixX:AttrEditor_MatrixXX,VectorX:AttrEditor_VectorX,Se3FakeType:AttrEditor_Se3,AlignedBox3:AttrEditor_AlignedBox3,AlignedBox2:AttrEditor_AlignedBox2}
_fundamentalInitValues={bool:True,str:'',int:0,float:0.0,Quaternion:Quaternion.Identity,Vector3:Vector3.Zero,Matrix3:Matrix3.Zero,Vector6:Vector6.Zero,Vector6i:Vector6i.Zero,Vector3i:Vector3i.Zero,Vector2i:Vector2i.Zero,Vector2:Vector2.Zero,Se3FakeType:(Vector3.Zero,Quaternion.Identity),AlignedBox3:(Vector3.Zero,Vector3.Zero),AlignedBox2:(Vector2.Zero,Vector2.Zero),MatrixX:MatrixX(),VectorX:VectorX()}

_attributeGuessedTypeMap={woo.core.NodeList:(woo.core.Node,),woo.core.ObjectList:(woo.core.Object,) }

def hasActiveLabel(s):
    if not hasattr(s,'label') or not  s.label: return False
    for t in s._getAllTraits():
        if t.activeLabel==True: return True
    return False

class ObjQLabel(QFrame):
    def __init__(self,parent,label,tooltip,path,ser=None,elide=False,searchSlot=None):
        QFrame.__init__(self,parent)
        lay=QHBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0)
        self.qlabel=QLabel(self)
        self.path=path
        self.ser=ser
        self.minWd=-1
        self.setTextToolTip(label,tooltip,elide=elide)
        self.qlabel.linkActivated.connect(woo.qt.openUrl)
        if searchSlot:
            le=QLineEdit(self)
            le.setTextMargins(0,0,0,0)
            le.setMaximumWidth(50)
            le.setMaximumHeight(15)
            le.textChanged.connect(searchSlot)
            self.lineEdit=le
            self.setColor(-1)
            lay.addWidget(le)
            lay.addStretch(1)
        lay.addWidget(self.qlabel)
        if searchSlot:
            lay.addStretch(2)
    #def sizeHint(self): return QSize(self.minWd,20)
    #def minimumSizeHint(self): return QSize(self.minWd,20)
    def setColor(self,match):
        if match==1: color='PaleGreen'
        elif match==0: color='OrangeRed'
        else: color='gray'
        self.lineEdit.setStyleSheet('QLineEdit{ background: %s; }'%color)
    def setTextToolTip(self,label,tooltip,elide=False):
        # ignore UnicodeDecodeError (appears sometimes under Windows), perhaps there are non-ascii characters in docstrings somewhere?
        try:
            if elide:
                # label is the text description; elide it at some fixed width
                #self.minWd=100
                label=self.fontMetrics().elidedText(label,Qt.ElideRight,int(1.5*self.width()))
            else:
                pass
                #self.minWd=60
            self.qlabel.setText(label)
            #self.adjustSize()
            if tooltip or self.path: self.qlabel.setToolTip(('<b>'+self.path+'</b><br>' if self.path else '')+(tooltip if tooltip else ''))
            else: self.qlabel.setToolTip(None)
        except UnicodeDecodeError:
            self.setToolTip(None)
    def mousePressEvent(self,event):
        if event.button()!=Qt.MidButton:
            event.ignore(); return
        if self.ser and (event.modifiers() & Qt.AltModifier or event.modifiers() & Qt.ControlModifier):
            # open this object in new window
            se=ObjectEditor(self.ser,parent=None,showType=True,labelIsVar=True,showChecks=False,showUnits=False,objManip=True)
            se.show()
            return
        if self.path==None: return # no path set
        # middle button clicked, paste pasteText to clipboard
        cb=QApplication.clipboard()
        cb.setText(self.path,mode=QClipboard.Clipboard)
        cb.setText(self.path,mode=QClipboard.Selection) # X11 global selection buffer
        event.accept()

def _unicodeUnit(u): return (u if isinstance(u,str) else str(u,'utf-8'))

def makeLibraryBrowser(parentmenu,callback,types,libName='Library'):
    '''Create menu under *parentmenu* named *libName*. *types* are passed to :obj:`woo.objectlibrary.checkout`. *callback* is called with chosen (name,object) as arguments.'''
    import woo.objectlibrary
    # root=parentmenu.addMenu('libName')
    objs=woo.objectlibrary.checkout(types=types)
    menus={():parentmenu.addMenu(libName)}
    if objs:
        for key,val in objs.items():
            # create submenus
            for i in range(1,len(key)):
                if key[:i] not in menus:
                    menus[key[:i]]=menus[key[:i-1]].addMenu(key[i-1])
            # create menu item
            item=menus[key[:-1]].addAction(key[-1])
            item.triggered.connect(lambda checked, name=key,obj=val: callback(name,obj))
    else: menus[()].setEnabled(False) # disable library menu if there are no objects in there




class ObjectEditor(QFrame):
    "Class displaying and modifying attributes of a woo object, or of unrelated attributes of different objects."
    import collections
    import logging
    # each attribute has one entry associated with itself

    # maximum nesting level in the UI, to avoid cycles and freezes
    maxNest=6
    # maximum length of inline lists
    maxListLen=40 


    class EntryData:
        def __init__(self,obj,name,T,doc,groupNo,trait,containingClass,editor,label):
            self.obj,self.name,self.T,self.doc,self.trait,self.groupNo,self.containingClass,self.label=obj,name,T,doc,trait,groupNo,containingClass,label
            self.widget=None
            self.visible=True
            self.hidden=False # this overrides the "visible" attribute
            self.gridAndRow=(None,-1)
            self.widgets={} #{'label':None,'value':None,'unit':None}
            self.editor=editor
        def propertyId(self):
            try:
                return id(getattr(self.containingClass,self.name))
            except AttributeError: return None
        def unitChanged(self,ix0=-1,forceBaseUnit=False):
            if not self.trait.unit or len(self.trait.altUnits[0])==0: return
            c=self.unitLayout
            if not self.trait.multiUnit:
                #print self.name,self.containingClass.__name__,self.trait.unit
                ix=c.itemAt(0).widget().currentIndex() if not forceBaseUnit else 0
                w=self.widgets['value']
                if ix==0: # base unit:
                    w.multiplier=None
                    # print(self.obj,self.containingClass,self.name)
                    w.multiplierChanged('')
                else:
                    w.multiplier=self.trait.altUnits[0][ix-1][1]
                    # print 20*'!',self.trait.name,self.editor
                    w.multiplierChanged(u'%s × %g = %s'%(_unicodeUnit(self.trait.altUnits[0][ix-1][0]) if ix>0 else '',w.multiplier,_unicodeUnit(self.trait.unit[0])))
            else: # multiplier is a tuple applied to each column separately
                mult,msg=[],[]
                for i in range(len(self.trait.unit)):
                    if self.trait.altUnits[i]: # not empty, there is a combo box
                        ix=c.itemAt(i).widget().currentIndex() if not forceBaseUnit else 0
                        mult.append(None if ix==0 else self.trait.altUnits[i][ix-1][1])
                        if mult[-1]!=None: msg.append(u'%s × %g = %s'%(_unicodeUnit(self.trait.altUnits[i][ix-1][0]) if ix>0 else '-',mult[-1],_unicodeUnit(self.trait.unit[i])))
                        else: msg.append('')
                w=self.widgets['value']
                w.multiplier=tuple(mult)
                w.multiplierChanged('<br>'.join(msg))
        def toggleChecked(self,checked):
            self.widgets['value'].setEnabled(not checked)
            self.widgets['label'].setEnabled(not checked)
        def eval_hideIf(self):
            return self.trait.hideIf and eval(self.trait.hideIf,globals(),{'self':self.editor.ser})
        def setVisible(self,visible=None):
            if visible==None: visible=self.visible
            self.visible=visible
            # if visible, the entry can be nevertheless hidden due to hideIf
            self.hidden=self.eval_hideIf()
            if not self.visible or self.hidden:
                for w in self.widgets.values():
                    if w: w.hide()
            else:
                for w in self.widgets.values():
                    if 'unit' in self.widgets and w==self.widgets['unit']: w.setVisible(self.editor.showUnits)
                    elif 'check' in self.widgets and w==self.widgets['check']: w.setVisible(self.editor.showChecks)
                    else: w.show()
            
            
    class EntryGroupData:
        def __init__(self,number,name):
            self.number=number
            self.name=name
            self.expander=None
            self.entries=[]
            style=QCommonStyle()
            self.rightArrow=style.standardIcon(QStyle.SP_ArrowRight)
            self.downArrow =style.standardIcon(QStyle.SP_ArrowDown)
        def makeExpander(self,expanded):
            self.expander=QPushButton(self.name)
            self.expander.setCheckable(True)
            self.expander.setChecked(expanded)
            self.expander.setStyleSheet('text-align: left; padding: 0pt; padding-left: 6px; ')
            self.expander.setFocusPolicy(Qt.StrongFocus)
            self.expander.toggled.connect(self.toggleExpander)
            self.setExpanderIcon()
            return self.expander
        def setExpanderIcon(self): self.expander.setIcon(self.downArrow if self.expander.isChecked() else self.rightArrow)

        def toggleExpander(self):
            with WidgetUpdatesDisabled(self.expander.parentWidget()):
                self.setExpanderIcon()
                for e in self.entries:
                    e.setVisible(self.expander.isChecked())


    def __init__(self,ser,parent=None,ignoredAttrs=set(),objAttrLabelList=None,showType=False,path=None,labelIsVar=True,showChecks=False,showUnits=False,objManip=True,nesting=0):
        "Construct window, *ser* is the object we want to show."
        # print path
        QFrame.__init__(self,parent)
        self.ser=ser
        self.oneObject=ser
        self.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Expanding)
        #where do widgets go in the grid
        self.gridCols={'check':0,'label':1,'value':2,'unit':3}
        # set path or use label, if active (allows 'label' attributes which don't imply automatic python variables of that name)
        self.path=('woo.master.scene.lab.'+ser.label if ser!=None and hasActiveLabel(ser) else path)
        self.showType=showType
        self.nesting=nesting
        self.labelIsVar=labelIsVar # show variable name; if false, docstring is used instead
        self.showChecks=showChecks
        self.showUnits=showUnits
        self.objManip=objManip
        self.hot=False
        self.entries=[]
        self.entryGroups=[]
        self.ignoredAttrs=ignoredAttrs
        self.hasSer=True
        self.searchText=None
        self.prevSearchText=None
        self.objQLabel=None

        if nesting>ObjectEditor.maxNest:
            self.mkStub()
            return
        
        if objAttrLabelList:
            self.hasSer=False
            # create entries for given attributes
            self.addListObjAttrEntries(objAttrLabelList)
        else:
            # no objAttrLabelList
            if self.ser==None:
                log.debug('New None Object')
                # show None
                lay=QGridLayout(self); lay.setContentsMargins(2,2,2,2); lay.setVerticalSpacing(0)
                lab=QLabel('<b>None</b>'); lab.setFrameShape(QFrame.Box); lab.setFrameShadow(QFrame.Sunken); lab.setLineWidth(2); lab.setAlignment(Qt.AlignHCenter);
                if self.showType: lay.addWidget(lab,0,0,1,-1)
                return # no timers, nothing will change at all
            log.debug('New Object of type %s'%ser.__class__.__name__)
        # create entries for all attributes of this object
        if ser:
            self.setWindowTitle(str(ser))
            self.addSerAttrEntries()
        with WidgetUpdatesDisabled(self): self.mkWidgets()
        
        self.refreshTimer=QTimer(self)
        self.refreshTimer.timeout.connect(self.refreshEvent)
        self.refreshTimer.start(500)

    def addListObjAttrEntries(self,objAttrLabelList):
        for obj,attr,label in objAttrLabelList:
            if not isinstance(obj,woo.core.Object): logging.error('%s is not a woo.core.Object (attribtue %s requested)'%(str(obj),attr))
            ii=[i for i in obj._getAllTraitsWithClasses() if i[0].name==attr]
            if len(ii)!=1:
                if not ii: logging.error('Object %s has no attribute trait %s.'%(obj.__class__.__name__,attr))
                else: logging.error('Object %s has multiple attribute traits named %s??'%(obj.__class__.__name__,attr))
                continue
            trait,klass=ii[0]
            self.addAttrEntry(obj=obj,trait=trait,klass=klass,label=label)

    def addSerAttrEntries(self):
        for trait,klass in self.oneObject._getAllTraitsWithClasses():
            self.addAttrEntry(obj=self.ser,trait=trait,klass=klass)

    def addAttrEntry(self,obj,trait,klass,label=None):
        'Return attribute entry where *obj* is the parent object, *trait* is the attribute trait, *klass* is the class of the attribute (as python type; if 1-tuple, list of those types). *label* is what will be shown in the UI; if omitted, trait.name will be used by default.'
        if trait.hidden or trait.noGui: return
        attr=trait.name
        try:
            val=getattr(obj,attr) # get the value using serattt, as it might be different from what the dictionary provides (e.g. Body.blockedDOFs)
        except TypeError: # no conversion possible
            logging.error("To-python conversion failed for %s.%s!"%(obj.__class__.__name__,trait.name))
            return
        t=None
        doc=trait.doc
        # remove sphinx markup from docstrings
        doc=re.sub(':[a-zA-Z0-9_-]+:`([^`]*)`',r'<i>\1</i>',doc)
        doc=re.sub('``([^`]+)``',r'<tt>\1</tt>',doc)

        if attr in self.ignoredAttrs: return

        # this attribute starts a new attribute group
        if trait.startGroup: self.entryGroups.append(self.EntryGroupData(number=len(self.entryGroups),name=trait.startGroup))

        # determine entry type
        if isinstance(val,list):
            t=woo.document.guessListTypeFromCxxType(obj.__class__,trait,warnFail=True)
            if not t and len(val)==1: t=(val[0].__class__,) # 1-tuple is list of the contained type
            #if not t: raise RuntimeError('Unable to guess type of '+str(obj)+'.'+attr)
        elif val.__class__ in _attributeGuessedTypeMap: t=_attributeGuessedTypeMap[val.__class__]
        elif not isinstance(val,woo.core.Object) and val is not None: t=val.__class__
        else: # for Woo objects, determine base class if manipulation is allowed; if not, use current instance type (it can't be changed anyway) or Object (it the value is None)
            if self.objManip: t=woo.document.guessInstanceTypeFromCxxType(obj.__class__,trait)
            elif val!=None: t=val.__class__
            else: t=Object

        if len(self.entryGroups)==0: self.entryGroups.append(self.EntryGroupData(number=0,name=None))
        groupNo=len(self.entryGroups)-1

        #if not match: print 'No attr match for docstring of %s.%s'%(obj.__class__.__name__,attr)
        #log.debug('Attr %s is of type %s'%(attr,((t[0].__name__,) if isinstance(t,tuple) else t.__name__)))
        self.entries.append(self.EntryData(obj=obj,name=attr,T=t,groupNo=groupNo,doc=doc,trait=trait,containingClass=klass,editor=self,label=label))

    def getDocstring(self,attr=None):
        "If attr is *None*, return docstring of the Object itself"
        if attr==None:
            if self.oneObject.__class__.__doc__!=None:
                doc=self.oneObject.__class__.__doc__
            else:
                logging.error('Class %s __doc__ is None?'%self.ser.__class__.__name__)
                return None
        else:
            ee=[e.doc for e in self.entries if e.name==attr]
            if not ee:
                logging.error('No entry for attribute named %s?'%(attr))
                doc='[no documentation found]'
            else: doc=e[0].doc
        # doc=re.sub(':[a-zA-Z0-9_-]+:`([^`]*)`',r'<i>\1</i>',doc)
        import textwrap
        wrapper=textwrap.TextWrapper(replace_whitespace=False)
        return wrapper.fill(textwrap.dedent(doc))

    def handleRanges(self,getter,setter,entry):
        rg=entry.trait.range
        # return editor for given attribute; no-op, unless float with associated range attribute
        if entry.T==float and rg and rg.__class__==Vector2:
            # getter returns tuple value,range
            # setter needs just the value itself
            return AttrEditor_FloatRange(self,lambda: (getattr(entry.obj,entry.name),rg),lambda x: setattr(entry.obj,entry.name,x))
        elif entry.T==int and rg and rg.__class__==Vector2i:
            return AttrEditor_IntRange(self,lambda: (getattr(entry.obj,entry.name),rg),lambda x: setattr(entry.obj,entry.name,x))
        # range for sequences has the special meaning of minimum and maximum lenth; handled in SeqEditor, not here
        elif isinstance(entry.T,(list,tuple)) and len(entry.T)==1: return None
        else:
            raise RuntimeError("Invalid range object for "+entry.obj.__class__.__name__+"."+entry.name+": type is "+str(entry.T)+", range is "+str(rg)+" (of type "+rg.__class__.__name__+")")
    def handleChoices(self,getter,setter,entry):
        # choice for sequences has the special meaning of descriptions of individual items; handled in SeqEditor, not here
        if isinstance(entry.T,(list,tuple)) and len(entry.T)==1: return None
        choice=entry.trait.choice
        return AttrEditor_Choice(self,lambda: (getattr(entry.obj,entry.name),choice),lambda x: setattr(entry.obj,entry.name,x),isColormap=entry.trait.colormap)
    def handleBits(self,getter,setter,entry):
        bits=entry.trait.bits
        return AttrEditor_Bits(self,lambda: (getattr(entry.obj,entry.name),bits),lambda x: setattr(entry.obj,entry.name,x))
    def handleRgbColor(self,getter,setter,entry):
        return AttrEditor_RgbColor(self,getter,setter)
    def handleFileDir(self,getter,setter,entry):
        return AttrEditor_FileDir(self,getter,setter,isDir=entry.trait.dirname,isExisting=entry.trait.existingFilename)
        
    def mkWidget(self,entry):
        if not entry.T:
            #print 'return None for %s.%s'%(entry.obj.__class__.__name__,entry.name)
            return None
        

        # single fundamental object
        widget=None
        # default getter and setter
        getter,setter=lambda: getattr(entry.obj,entry.name), lambda x: setattr(entry.obj,entry.name,x)
        # try to find specific widget first based on traits
        # these functions may return none, indicating that it won't be handled specially
        if entry.trait.range: widget=self.handleRanges(getter,setter,entry)
        elif entry.trait.choice: widget=self.handleChoices(getter,setter,entry)
        elif entry.trait.bits: widget=self.handleBits(getter,setter,entry)
        elif entry.trait.rgbColor: widget=self.handleRgbColor(getter,setter,entry)
        elif entry.trait.filename or entry.trait.existingFilename or entry.trait.dirname: widget=self.handleFileDir(getter,setter,entry)
        # no specific widget found, try one for fundamental types
        else:
            Klass=_fundamentalEditorMap.get(entry.T,None)
            if Klass: widget=Klass(self,getter=getter,setter=setter)
        if widget:
            widget.setFocusPolicy(Qt.StrongFocus)
            if entry.trait.readonly: widget.setEnabled(False)
            return widget

        # sequences
        if entry.T.__class__==tuple:
            assert len(entry.T)==1 # we don't handle tuples of other lengths
            # sequence of serializables
            T=entry.T[0]
            if (issubclass(T,Object) or T==Object):
                # HACK!!!
                # per-instance traits for py-derived objects
                if hasattr(entry.obj,'_instanceTraits') and entry.trait.name in entry.obj._instanceTraits:
                    entry.trait=entry.obj._instanceTraits[entry.trait.name]
                widget=SeqObject(self,getter,setter,T=T,trait=entry.trait,path=(self.path+'.'+entry.name if self.path else None),shrink=True,nesting=self.nesting+1)
                return widget
            if (T in _fundamentalEditorMap):
                widget=SeqFundamentalEditor(self,getter,setter,T)
                return widget
            print('No widget for (%s,) in %s.%s'%(T.__name__,entry.obj.__class__.__name__,entry.name))
            return None
        # a woo.Object
        if issubclass(entry.T,Object) or entry.T==Object:
            obj=getattr(entry.obj,entry.name)
            # should handle the case of obj==None as well
            widget=ObjectEditor(getattr(entry.obj,entry.name),parent=self,showType=self.showType,path=(self.path+'.'+entry.name if self.path else None),labelIsVar=self.labelIsVar,showChecks=self.showChecks,showUnits=self.showUnits,objManip=self.objManip,nesting=self.nesting+1)
            widget.setFrameShape(QFrame.Box); widget.setFrameShadow(QFrame.Raised); widget.setLineWidth(1)
            return widget
        print('No widget for %s in %s.%s'%(entry.T.__name__,entry.obj.__class__.__name__,entry.name))
        return None
    def serQLabelMenu(self,widget,position):
        menu=QMenu(self)
        toggleLabelIsVar=menu.addAction('Variables')
        toggleLabelIsVar.setCheckable(True); toggleLabelIsVar.setChecked(self.labelIsVar)
        toggleLabelIsVar.triggered.connect(lambda: self.toggleLabelIsVar(None))
        toggleShowChecks=menu.addAction(u'Checks')
        toggleShowChecks.setCheckable(True); toggleShowChecks.setChecked(self.showChecks)
        toggleShowChecks.triggered.connect(lambda: self.toggleShowChecks(None))
        toggleShowUnits=menu.addAction('Units')
        toggleShowUnits.setCheckable(True); toggleShowUnits.setChecked(self.showUnits)
        toggleShowUnits.triggered.connect(lambda: self.toggleShowUnits(None))
        if self.hasSer and self.ser is not None:
            actionSave=menu.addAction(u'⛁ Save')
            actionSave.triggered.connect(lambda: self.saveObject(self.ser))
            # actionSave.triggered.connect(lambda obj=self.ser: self.saveObject(obj))
        #if self.path is not None:
        #    actionLoad=menu.addAction(u'↥ Load')
        #    actionLoad.trigger.connect(lambda: self.loadObject)
        menu.popup(self.mapToGlobal(position))
        #print 'menu popped up at ',widget.mapToGlobal(position),' (local',position,')'
    def saveObject(self,obj):
        # print('saveObject',obj,arg2,arg3,arg4)
        assert obj is not None
        f=QFileDialog.getSaveFileName(self,'Save object: use .json, .expr, .pickle, .html ...','.')
        if isinstance(f,tuple): f=f[0]
        print('Saving to:',f)
        if not f: return
        woo._monkey.io.Object_dump(obj,str(f),format='auto',fallbackFormat='expr')
    # XXX: deprecated chunk
    #def loadObject(self):
    #    assert self.path
    #    f=QFileDialog.getOpenFileName(self,msg,'.')
    #    if not f: return # no file selected
    #    try:
    #        if isObj: obj=entry.T.load(f) # be user friendly if garbage is being loaded
    #        else: obj=woo._monkey.io.Object_load(None,f)
    #        raise NotImplementedError('Loading objects to path is not yet implemented.')
    #        # get parent object (or parent sequence!) and use setattr/setitem to assign the object
    #        # parent=path.split('.').join('.')
    #        # eval(path)=obj
    #    except Exception as e:
    #        import traceback
    #        traceback.print_exc()
    #        showExceptionDialog(self,e)
    def getAttrLabelToolTip(self,entry):
        try:
            ini=str(entry.trait.ini) if (entry.trait.ini and not isinstance(entry.trait.ini,Object)) else ''
        except TypeError:
            # boost::python won't convert weak_ptr, catch it here
            ini=''
        toolTip=entry.containingClass.__name__+u'.<b><i>'+entry.name+u'</i></b><br>'+_ensureUnicode(entry.doc)+u'<br><small>default: %s</small>'%ini
        if self.labelIsVar: return woo.document.makeObjectHref(entry.obj,entry.name,text=entry.label),toolTip
        return entry.doc,toolTip
    def toggleLabelIsVar(self,val=None):
        self.labelIsVar=(not self.labelIsVar if val==None else val)
        for entry in self.entries:
            entry.widgets['label'].setTextToolTip(*self.getAttrLabelToolTip(entry),elide=not self.labelIsVar)
            if entry.widget.__class__==ObjectEditor:
                entry.widget.toggleLabelIsVar(self.labelIsVar)
    def toggleShowChecks(self,val=None):
        with WidgetUpdatesDisabled(self):
            self.showChecks=(not self.showChecks if val==None else val)
            #for g in self.entryGroups: g.showChecks=self.showChecks # propagate down
            for entry in self.entries:
                if entry.visible and not entry.hidden: entry.widgets['check'].setVisible(self.showChecks)
                if not entry.trait.readonly:
                    if 'value' in entry.widgets: entry.widgets['value'].setEnabled(True)
                    entry.widgets['label'].setEnabled(True)
                if entry.widget.__class__==ObjectEditor:
                    entry.widget.toggleShowChecks(self.showChecks)
    def toggleShowUnits(self,val=None):
        with WidgetUpdatesDisabled(self):
            self.showUnits=(not self.showUnits if val==None else val)
            #print self.showUnits
            for entry in self.entries:
                entry.setVisible(None)
                entry.unitChanged(forceBaseUnit=(not self.showUnits))
                if entry.widget.__class__==ObjectEditor:
                    entry.widget.toggleShowUnits(self.showUnits)
    def objManipLabelMenu(self,entry,pos):
        'context menu for creating/deleting/loading/saving woo.core.Object from within the editor'
        menu=QMenu(self)
        isNone=(getattr(entry.obj,entry.name)==None)
        # isinstance is false for None, but None is always (?) missing woo.core.Object anyway
        isObj=isinstance(getattr(entry.obj,entry.name),woo.core.Object) or isNone
        default=menu.addAction(u'↺ Default')
        default.triggered.connect(lambda: self.doObjManip('default',entry,isNone,isObj))
        if isObj:
            if isNone:
                newDel=menu.addAction(u'☘ New')
                newDel.triggered.connect(lambda: self.doObjManip('new',entry,isNone,isObj))
            else:
                d=menu.addAction(u'☠  Delete')
                d.triggered.connect(lambda: self.doObjManip('del',entry,isNone,isObj))
                replace=menu.addAction(u'⇄ Replace')
                replace.triggered.connect(lambda: self.doObjManip('replace',entry,isNone,isObj))
        if not isNone:
            save=menu.addAction(u'⛁ Save')
            save.triggered.connect(lambda: self.doObjManip('save',entry,isNone,isObj))
        load=menu.addAction(u'↥ Load')
        load.triggered.connect(lambda: self.doObjManip('load',entry,isNone,isObj))
        lib=makeLibraryBrowser(menu,lambda name,obj: self.setFromLib(entry,name,obj),entry.T,u'⇈ Library')
        # put this into each varibles menus instead, as there is no the label at the top
        if not self.showType or not self.hasSer:
            menu.addSeparator()
            v=menu.addAction('Variables')
            v.setCheckable(True); v.setChecked(self.labelIsVar)
            v.triggered.connect(lambda: self.toggleLabelIsVar(None))
            c=menu.addAction(u'Checks')
            c.setCheckable(True); c.setChecked(self.showChecks)
            c.triggered.connect(lambda: self.toggleShowChecks(None))
            u=menu.addAction('Units')
            u.setCheckable(True); u.setChecked(self.showUnits)
            u.triggered.connect(lambda: self.toggleShowUnits(None))


        menu.popup(entry.widgets['label'].mapToGlobal(pos))
    def setFromLib(self,entry,name,obj):
        # setting library object
        if isinstance(obj,woo.core.Object): obj=obj.deepcopy() # prevent lib object modification
        setattr(entry.obj,entry.name,obj)
    def doObjManip(self,action,entry,isNone,isObj):
        'Handle menu action from objManipLabelMenu'
        # FIXME: this is an ugly hack of using woo._monkey.io.Object_{dump,load} directly!!!
        import woo, woo.core, woo._monkey.io
        #print 'Manipulating Object',entry.obj.__class__.__name__+'.'+entry.name
        if action=='del':
            assert isObj
            setattr(entry.obj,entry.name,None)
        elif action=='new' or action=='replace':
            assert isObj
            types=woo.system.childClasses(entry.T,includeBase=True)
            if(len(types)==1): setattr(entry.obj,entry.name,entry.T())
            else:
                d=NewObjectDialog(self,entry.T)
                if not d.exec_(): return # cancelled
                setattr(entry.obj,entry.name,d.result())
        elif action=='save':
            assert not isNone
            obj=getattr(entry.obj,entry.name)
            f=QFileDialog.getSaveFileName(self,'Save object: use .json, .expr, .pickle, .html ...','.')
            if isinstance(f,tuple): f=f[0]
            if not f: return
            woo._monkey.io.Object_dump(obj,str(f),format='auto',fallbackFormat='expr')
        elif action=='load':
            msg='Load a %s'%entry.T.__name__ if isObj else 'Load'
            f=QFileDialog.getOpenFileName(self,msg,'.')
            if isinstance(f,tuple): f=f[0]
            if not f: return # no file selected
            try:
                if isObj: obj=entry.T.load(f) # be user friendly if garbage is being loaded
                else: obj=woo._monkey.io.Object_load(None,f)
                setattr(entry.obj,entry.name,obj)
            except Exception as e:
                import traceback
                traceback.print_exc()
                showExceptionDialog(self,e)
        elif action=='default':
            if isObj:
                val=entry.trait.ini.deepcopy() if entry.trait.ini!=None else None # don't call deepcopy on none, it fails :)
            else:
                import copy
                val=copy.deepcopy(entry.trait.ini)
            setattr(entry.obj,entry.name,val)
        else: raise RuntimError('Unknown action %s for object manipulation context menu!'%action)
        self.refreshEvent()

    def mkStub(self):
        lay=QGridLayout(self)
        lay.setContentsMargins(2,2,2,2)
        lay.setVerticalSpacing(0)
        self.setLayout(lay)
        lay.addWidget(QLabel("GUI nesting > %d"%(ObjectEditor.maxNest)),1,1)

    
    def mkWidgets(self):
        onlyDefaultGroups=(len(self.entryGroups)==1 and self.entryGroups[0].name==None)
        if self.showType and self.hasSer: # create type label
            lab=ObjQLabel(self,makeObjectLabel(self.ser,addr=True,href=True),tooltip=self.getDocstring(),path=self.path,ser=self.ser,searchSlot=self.search)
            self.objQLabel=lab
            lab.setFrameShape(QFrame.Box); lab.setFrameShadow(QFrame.Sunken); lab.setLineWidth(2); lab.qlabel.setAlignment(Qt.AlignHCenter); lab.qlabel.linkActivated.connect(woo.qt.openUrl)
            ## attach context menu to the label
            lab.setContextMenuPolicy(Qt.CustomContextMenu)
            lab.customContextMenuRequested.connect(lambda pos: self.serQLabelMenu(lab,pos))
            lab.setFocusPolicy(Qt.ClickFocus)
        lay=QGridLayout(self)
        lay.setContentsMargins(2,2,2,2)
        lay.setVerticalSpacing(0)
        if self.showType:
            lay.addWidget(lab,0,0,1,-1)
            lay.setRowStretch(0,-1)
        self.setLayout(lay)

        maxLabelWd=0
        for entry in self.entries:
            w=self.mkWidget(entry)
            if w!=None: entry.widget=entry.widgets['value']=w
            # else: print 'No value widget for',entry.obj.__class__.__name__+'.'+entry.name
            #entry.widgets['value']=entry.widget # for code compat
            #if not entry.widgets['value']: entry.widgets['value']=entry.widget=QFrame() # avoid None widgets
            objPath=(self.path+'.'+entry.name) if self.path else None
            labelText,labelTooltip=self.getAttrLabelToolTip(entry)
            label=ObjQLabel(self,labelText,tooltip=labelTooltip,path=objPath,elide=not self.labelIsVar)
            entry.widgets['label']=label
            if self.objManip and ('value' in entry.widgets):
                label.setContextMenuPolicy(Qt.CustomContextMenu)
                label.customContextMenuRequested.connect(lambda pos,entry=entry: self.objManipLabelMenu(entry,pos))
                label.setFocusPolicy(Qt.ClickFocus)
            entry.widgets['check']=QCheckBox('',self)
            ch=entry.widgets['check']
            ch.setVisible(self.showChecks)
            if entry.trait.readonly: ch.setEnabled(False)
            ch.clicked.connect(entry.toggleChecked)
            if entry.trait.unit:
                # frame for all unit-manipulating boxes
                entry.widgets['unit']=QFrame(self)
                unitLay=QHBoxLayout(entry.widgets['unit'])
                entry.unitLayout=unitLay
                unitLay.setSpacing(0); unitLay.setContentsMargins(0,0,0,0)
                entry.widgets['unit'].setLayout(unitLay)
                assert len(entry.trait.unit)==len(entry.trait.altUnits)
                assert len(entry.trait.unit)==len(entry.trait.prefUnit)
                unitChoice=False
                for unit,pref,alt in zip(entry.trait.unit,entry.trait.prefUnit,entry.trait.altUnits):
                    if alt: # there are alternative units, we give choice therefore
                        unitChoice=True
                        w=QComboBox(self)
                        w.addItem(_unicodeUnit(unit))
                        w.activated.connect(entry.unitChanged)
                        for u,mult in alt: w.addItem(_unicodeUnit(u))
                        # set preferred unit right away; when units are not shown, always use SI, however
                        if unit!=pref[0]:
                            # this is checked in c++, should never fail
                            # print entry.trait.name,[i for i in range(len(alt)) if alt[i][0]==pref[0]]
                            ii=[i for i in range(len(alt)) if alt[i][0]==pref[0]][0]
                            w.setCurrentIndex(ii+1)
                    else:
                        w=QLabel(_unicodeUnit(unit),self)
                    unitLay.addWidget(w)
                if unitChoice: entry.unitChanged() # postpone calling this at the very end
                entry.widgets['unit'].setVisible(self.showUnits)
            if entry.trait.buttons:
                bb=entry.trait.buttons[0]
                for i in range(0,len(bb),3):
                    b=QPushButton(bb[i],self)
                    l=QLabel(self.fontMetrics().elidedText(' '+bb[i+2],Qt.ElideRight,100))
                    b.setToolTip('<code>'+bb[i+1]+'</code>')
                    b.setStyleSheet('QPushButton {text-align: left; padding-left:5px; }')
                    b.setFocusPolicy(Qt.NoFocus)
                    l.setToolTip(bb[i+2])
                    def callButton(cmd):
                        exec(cmd, globals(),dict(self=entry.obj,S=woo.master.scene))
                    # first arg (foo) used by the dispatch
                    # cmd=bb[i+1] binds the current value bb[i+1]
                    b.clicked.connect(lambda foo,cmd=bb[i+1]: callButton(cmd)) 
                    entry.widgets['buttons-%d'%(i//3)]=b
                    entry.widgets['buttonLabels-%d'%(i//3)]=l 
                #print 'Buttons',entry.trait.name,entry.trait.buttons,entry.widgets
            self.entryGroups[entry.groupNo].entries.append(entry)
        for i,g in enumerate(self.entryGroups):
            hide=i>0
            if not onlyDefaultGroups:
                ex=g.makeExpander(not hide) # first group expanded, other hidden
                lay.addWidget(ex,lay.rowCount(),0,1,-1)
                lay.setRowStretch(lay.rowCount()-1,-1)
            for i,entry in enumerate(g.entries):
                try:
                    def addButtonsNow():
                        bb,ll=sorted([k for k in entry.widgets.keys() if k.startswith('buttons-')]),sorted([k for k in entry.widgets.keys() if k.startswith('buttonLabels-')])
                        for b,l in zip(bb,ll):
                            row=lay.rowCount()
                            lay.addWidget(entry.widgets[b],row,self.gridCols['value'],1,1)
                            lay.addWidget(entry.widgets[l],row,self.gridCols['label'],1,1)
                            entry.widgets[l].setStyleSheet('background: palette(%s); '%('Base' if row%2 else 'AlternateBase'))
                    # add buttons before
                    if entry.trait.buttons and entry.trait.buttons[1]==True: addButtonsNow()
                    row=lay.rowCount()
                    entry.gridAndRow=lay,row
                    lay.addWidget(entry.widgets['check'],row,self.gridCols['check'],1,1)
                    lay.addWidget(entry.widgets['label'],row,self.gridCols['label'],1,1)
                    entry.widgets['check'].setFocusPolicy(Qt.ClickFocus)
                    # entry.widgets['label'].setFocusPolicy(Qt.NoFocus) # default
                    maxLabelWd=max(maxLabelWd,entry.widgets['label'].width())
                    if 'value' in entry.widgets:
                        colSpan=(2 if 'unit' not in entry.widgets else 1) # use the unit column if there is no unit
                        lay.addWidget(entry.widgets['value'],row,self.gridCols['value'],1,colSpan)
                        # entry.widgets['value'].setFocusPolicy(Qt.StrongFocus) # default
                    if 'unit' in entry.widgets:
                        lay.addWidget(entry.widgets['unit'],row,self.gridCols['unit'])
                        entry.widgets['unit'].setFocusPolicy(Qt.ClickFocus) # skip when keyboard-navigating
                    lay.setRowStretch(row,2)
                    for w in entry.widgets['label'],: # entry.widgets['value']:
                        if not w or w.__class__==ObjectEditor: continue # nested editor not modified
                        w.setStyleSheet('background: palette(%s); '%('Base' if row%2 else 'AlternateBase'))
                    # add buttons after
                    if entry.trait.buttons and entry.trait.buttons[1]==False: addButtonsNow()
                except RuntimeError:
                    print('ERROR while creating widget for entry %s (%s)'%(entry.name,objPath))
                    import traceback
                    traceback.print_exc()
        # close all groups except the first one            
        for g in self.entryGroups[1:]: g.toggleExpander()
        lay.setColumnMinimumWidth(self.gridCols['label'],int(maxLabelWd))
        lay.setSpacing(0)
        lay.addWidget(QFrame(self),lay.rowCount(),0,1,-1) # expander at the very end
        lay.setRowStretch(lay.rowCount()-1,10000)
        lay.setColumnStretch(self.gridCols['check'],-1)
        lay.setColumnStretch(self.gridCols['label'],2)
        lay.setColumnStretch(self.gridCols['value'],10)
        lay.setColumnStretch(self.gridCols['unit'],-1)
        self.refreshEvent()

    def search(self,text):
        self.prevSearchText=self.searchText
        self.searchText=str(text)
        self.refreshEvent()

    def refreshEvent(self):
        maxLabelWd=0.
        # restore group visibility, since there is no search text anymore
        if not self.searchText and self.prevSearchText:
            for g in self.entryGroups:
                g.toggleExpander() # queries the group expander whether it is toggled or not
            self.prevSearchText=''
            self.objQLabel.setColor(-1)
        searchMatches=0

        for e in self.entries:
            if self.hasSer: assert self.ser==e.obj
            if e.widget and not e.widget.hot:
                maxLabelWd=max(maxLabelWd,e.widgets['label'].width())
                # if there is a new instance of Object, we need to make new widget and replace the old one completely
                if type(e.widget)==ObjectEditor and e.widget.hasSer and e.widget.ser!=getattr(e.obj,e.name):
                    # print 'New ObjectEditor (%s): '%e.name,e.widget.ser,'->',getattr(e.obj,e.name)
                    # print e.widget.ser._cxxAddr,getattr(e.obj,e.name)._cxxAddr,getattr(e.obj,e.name)
                    assert e.widget.ser==None or getattr(e.obj,e.name)==None or e.widget.ser._cxxAddr!=getattr(e.obj,e.name)._cxxAddr or (isinstance(getattr(e.obj,e.name),woo.pyderived.PyWooObject) and id(getattr(e.obj,e.name))!=id(e.widget.ser))
                    e.widget.hide()
                    e.widget=e.widgets['value']=self.mkWidget(e)
                    grid,row=e.gridAndRow
                    colSpan=(2 if 'unit' not in e.widgets else 1)
                    grid.addWidget(e.widget,row,self.gridCols['value'],1,colSpan)
                ## propagate the search to nested widgets; disabled for now, since the widget itself will be very likely disabled anyway, as the search result does not propagate upwards to us
                # if type(e.widget)==ObjectEditor: e.searchText=self.searchText
                if not self.searchText:
                    # visibility might change if hideIf is defined
                    if e.trait.hideIf or not e.visible: e.setVisible(None)
                else:
                    if re.search(self.searchText,e.name,re.IGNORECASE):
                        searchMatches+=1
                        e.setVisible(True)
                    else: e.setVisible(False)
                e.widget.refresh()
        if self.searchText: self.objQLabel.setColor(1 if searchMatches else 0)
        #self.layout().setColumnMinimumWidth(self.gridCols['label'],maxLabelWd)
        if self.labelIsVar: self.layout().setColumnStretch(self.gridCols['label'],-1)
        else: self.layout().setColumnStretch(self.gridCols['label'],2)
    def refresh(self): pass

def makeObjectLabel(ser,href=False,addr=True,boldHref=True,num=-1,count=-1):
    ret=u''
    if num>=0:
        if count>=0: ret+=u'%d/%d. '%(num,count)
        else: ret+=u'%d. '%num
    if href: ret+=(u' <b>' if boldHref else u' ')+woo.document.makeObjectHref(ser)+(u'</b> ' if boldHref else u' ')
    else: ret+=ser.__class__.__name__+' '
    if hasActiveLabel(ser): ret+=u' “'+str(ser.label)+u'”'
    # do not show address if there is a label already
    elif addr and ser!=None:
        ret+='0x%x'%ser._cxxAddr
    return ret

class SeqObjectComboBox(QFrame):
    maxShowLen=500
    def getItemType(self): return self.trait.pyType[0]
    def __init__(self,parent,getter,setter,T,trait,path=None,shrink=False,nesting=0):
        QFrame.__init__(self,parent)
        self.getter,self.setter,T,self.trait,self.path,self.shrink=getter,setter,T,trait,path,shrink
        #if not hasattr(self.trait,'pyType'): self.trait.pyType=(T,) # this is for compat with C++ (?)
        if self.trait.pyType is None: self.trait.pyType=(T,)
        self.hot=None # API compat with ObjectEditor
        ll=len(self.getter())
        if ll>self.maxShowLen:
            lab=QLabel('<i>Not showing sequence with %d (&gt;%d) items.</i>'%(ll,self.maxShowLen))
            lay=QVBoxLayout(self)
            lay.addWidget(lab)
            self.setLayout(lay)
            return
        self.layout=QVBoxLayout(self)
        self.nesting=nesting
        topLineFrame=QFrame(self)
        topLineLayout=QHBoxLayout(topLineFrame);
        topLineFrame.setLayout(topLineLayout)
        for l in self.layout, topLineLayout: l.setSpacing(0); l.setContentsMargins(0,0,0,0)
        labels=(u'+',u'−',u'↑',u'↓')
        tooltips=('Add','Delete','Move up','Move down')
        buttons=(self.newButton,self.killButton,self.upButton,self.downButton)=[QPushButton(label,self) for label in labels]
        buttonSlots=(None,self.killSlot,self.upSlot,self.downSlot) # same order as buttons
        for i,b in enumerate(buttons): b.setStyleSheet('QPushButton { font-size: 15pt; font-weight: bold; }'); b.setFixedWidth(25); b.setFixedHeight(30); b.setToolTip(tooltips[i])
        self.combo=QComboBox(self)
        self.combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        for w in buttons[:2]+[self.combo,]+buttons[2:]: topLineLayout.addWidget(w)
        self.layout.addWidget(topLineFrame) # nested layout
        self.scroll=QScrollArea(self); self.scroll.setWidgetResizable(True)
        self.layout.addWidget(self.scroll)
        self.seqEdit=None # currently edited serializable
        self.setLayout(self.layout)
        self.setFrameShape(QFrame.Box); self.setFrameShadow(QFrame.Raised); self.setLineWidth(1)
        self.newDialog=None # is set when new dialog is created, and destroyed when it returns
        self.comboItemCount=0 # we use some inactive items with ranges instead of objects, so keep track of valid items separately
        # create menu for the "new" button
        newMenu=QMenu();
        newMenu.addAction(u'☘ New',self.newSlot)
        self.cloneAction=newMenu.addAction(u'≡ Clone',self.cloneSlot)
        newMenu.addAction(u'↥ Load',self.loadSlot)
        lib=makeLibraryBrowser(newMenu,lambda name,obj: self.libLoadSlot(name,obj),self.getItemType(),u'⇈ Library')
        self.newButton.setMenu(newMenu)

        # signals
        for b,slot in zip(buttons[1:],buttonSlots[1:]): b.clicked.connect(slot)
        self.combo.currentIndexChanged.connect(self.comboIndexSlot)
        self.refreshEvent()
        # periodic refresh
        self.refreshTimer=QTimer(self)
        self.refreshTimer.timeout.connect(self.refreshEvent)
        self.refreshTimer.start(500) # 1s should be enough
        #print 'SeqObject path is',self.path
    def comboIndexSlot(self,ix): # different seq item selected
        currSeq=self.getter();
        if ix>=len(currSeq): # this can happen with fake items which get activated when real item is deleted
            self.combo.setCurrentIndex(len(currSeq)-1)
            return
        if len(currSeq)==0: ix=-1
        log.debug('%s comboIndexSlot len=%d, ix=%d'%(self.getItemType().__name__,len(currSeq),ix))
        self.downButton.setEnabled(ix<len(currSeq)-1)
        self.upButton.setEnabled(ix>0)
        self.combo.setEnabled(ix>=0)
        if ix>=0:
            ser=currSeq[ix]
            self.seqEdit=ObjectEditor(ser,parent=self,showType=seqObjectShowType,path=(self.path+'['+str(ix)+']') if self.path else None,nesting=self.nesting+1)
            self.scroll.setWidget(self.seqEdit)
            if self.shrink:
                self.sizeHint=lambda: QSize(100,1000)
                self.scroll.sizeHint=lambda: QSize(100,1000)
                self.sizePolicy().setVerticalPolicy(QSizePolicy.Expanding)
                self.scroll.sizePolicy().setVerticalPolicy(QSizePolicy.Expanding)
                self.setMinimumHeight(min(300,self.seqEdit.height()+self.combo.height()+10))
                self.setMaximumHeight(100000)
                self.scroll.setMaximumHeight(100000)
        else:
            self.scroll.setWidget(QFrame())
            if self.shrink:
                self.setMaximumHeight(self.combo.height()+10);
                self.scroll.setMaximumHeight(0)
    # XXX: deprecated chunk
    # def serLabel(self,ser,i=-1):
    #     return ('' if i<0 else str(i)+'. ')+str(ser)[1:-1].replace('instance at ','')
    def refreshEvent(self,forceIx=-1):
        try:
            currSeq=self.getter()
            comboEnabled=self.combo.isEnabled()
            if comboEnabled and len(currSeq)==0: self.comboIndexSlot(-1) # force refresh, otherwise would not happen from the initially empty state
            ix,cnt=self.combo.currentIndex(),self.comboItemCount # self.combo.count()
            # serializable currently being edited (which can be absent) or the one of which index is forced
            ser=(self.seqEdit.ser if self.seqEdit else None) if forceIx<0 else currSeq[forceIx] 
            if comboEnabled and len(currSeq)==cnt and (ix<0 or (ix<len(currSeq) and ser==currSeq[ix])): return
            if not comboEnabled and len(currSeq)==0: return
            log.debug(self.getItemType().__name__+' rebuilding list from scratch')
            self.combo.clear()
            if len(currSeq)>0:
                prevIx=-1
                for i,s in enumerate(currSeq):
                    extra=[]
                    if self.trait.choice and i<len(self.trait.choice): extra.append(self.trait.choice[i])
                    if self.trait.range and i>=self.trait.range[0]: extra.append('optional')
                    extra=' (%s)'%('; '.join(extra)) if extra else ''
                    self.combo.addItem(makeObjectLabel(s,num=i,count=len(currSeq),addr=False)+extra)
                    if s==ser: prevIx=i
                self.comboItemCount=self.combo.count()
                # add extra (inactive, unselectable) items
                # when using range and descriptions
                if self.trait.range or self.trait.choice:
                    for i in range(len(currSeq),max(self.trait.range[1],len(self.trait.choice))):
                        extra=[]
                        if self.trait.choice and i<len(self.trait.choice): extra.append(self.trait.choice[i])
                        if self.trait.range and i>=self.trait.range[0]: extra.append('optional')
                        extra=' (%s)'%('; '.join(extra)) if extra else ''
                        self.combo.addItem(u'%d. −'%i+extra)
                        # use the hack from http://theworldwideinternet.blogspot.cz/2011/01/disabling-qcombobox-items.html to deactivate items
                        self.combo.setItemData(i,0,Qt.UserRole-1)
                if forceIx>=0: newIx=forceIx # force the index (used from newSlot to make the new element active)
                elif prevIx>=0: newIx=prevIx # if found what was active before, use it
                elif ix>=0: newIx=ix         # otherwise use the previous index (e.g. after deletion)
                else: newIx=0                  # fallback to 0
                log.debug('%s setting index %d'%(self.getItemType().__name__,newIx))
                self.combo.setCurrentIndex(newIx)
            else:
                log.debug('%s EMPTY, setting index 0'%(self.getItemType().__name__))
                self.combo.setCurrentIndex(-1)
            enableKill=(not self.trait.noGuiResize and len(currSeq)>(self.trait.range[0] if self.trait.range else 0))
            enableNew=(not self.trait.noGuiResize and (not self.trait.range or len(currSeq)<self.trait.range[1]))
            enableClone=enableNew and len(currSeq)>0
            self.killButton.setEnabled(enableKill)
            self.newButton.setEnabled(enableNew)
            self.cloneAction.setEnabled(enableClone)
        except RuntimeError as e:
            print('Error refreshing sequence (path %s), ignored.'%self.path)
            
    def newSlot(self):
        # print 'newSlot called'
        dialog=NewObjectDialog(self,self.getItemType())
        if not dialog.exec_(): return # cancelled
        ser=dialog.result()
        ix=self.combo.currentIndex()
        currSeq=list(self.getter()); currSeq.insert(ix,ser); self.setter(currSeq)
        log.warning('%s new item created at index %d'%(self.getItemType().__name__,ix))
        self.refreshEvent(forceIx=ix)
    def loadSlot(self):
        f=QFileDialog.getOpenFileName(self,msg,'.')
        if isinstance(f,tuple): f=f[0]
        if not f: return # no file selected
        T=self.getItemType()
        try:
            obj=T.load(f) # be user friendly if garbage is being loaded
            # else: obj=woo._monkey.io.Object_load(None,f)
            currSeq=list(self.getter()); currSeq.insert(ix+1,obj); self.setter(currSeq)
        except Exception as e:
            import traceback
            traceback.print_exc()
            showExceptionDialog(self,e)
    def libLoadSlot(self,name,obj):
        ix=self.combo.currentIndex()
        currSeq=list(self.getter()); currSeq.insert(ix+1,obj); self.setter(currSeq)
        self.refreshEvent(forceIx=ix+1)
    def cloneSlot(self):
        ix=self.combo.currentIndex()
        currSeq=list(self.getter()); currSeq.insert(ix+1,currSeq[ix].deepcopy()); self.setter(currSeq)
        self.refreshEvent(forceIx=ix+1)
    def killSlot(self):
        ix=self.combo.currentIndex()
        currSeq=self.getter(); del currSeq[ix]; self.setter(currSeq)
        self.refreshEvent()
    def upSlot(self):
        i=self.combo.currentIndex()
        assert(i>0)
        currSeq=self.getter();
        prev,curr=currSeq[i-1:i+1]; currSeq[i-1],currSeq[i]=curr,prev; self.setter(currSeq)
        self.refreshEvent(forceIx=i-1)
    def downSlot(self):
        i=self.combo.currentIndex()
        currSeq=self.getter(); assert(i<len(currSeq)-1);
        curr,nxt=currSeq[i:i+2]; currSeq[i],currSeq[i+1]=nxt,curr; self.setter(currSeq)
        self.refreshEvent(forceIx=i+1)
    def refresh(self): pass # API compat with ObjectEditor

SeqObject=SeqObjectComboBox


class NewFundamentalDialog(QDialog):
    def __init__(self,parent,attrName,typeObj,typeStr):
        QDialog.__init__(self,parent)
        self.setWindowTitle('%s (type %s)'%(attrName,typeStr))
        self.layout=QVBoxLayout(self)
        self.scroll=QScrollArea(self)
        self.scroll.setWidgetResizable(True)
        self.buttons=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel);
        self.buttons.accepted.connect(self.accept)
        self.buttons.rejected.connect(self.reject)
        self.layout.addWidget(self.scroll)
        self.layout.addWidget(self.buttons)
        self.setWindowModality(Qt.WindowModal)
        class FakeObjClass: pass
        self.fakeObj=FakeObjClass()
        self.attrName=attrName
        Klass=_fundamentalEditorMap.get(typeObj,None)
        initValue=_fundamentalInitValues.get(typeObj,typeObj())
        setattr(self.fakeObj,attrName,initValue)
        if Klass:
            self.widget=Klass(None,self.fakeObj,attrName)
            self.scroll.setWidget(self.widget)
            self.scroll.show()
            self.widget.refresh()
        else: raise RuntimeError("Unable to construct new dialog for type %s"%(typeStr))
    def result(self):
        self.widget.update()
        return getattr(self.fakeObj,self.attrName)

class NewObjectDialog(QDialog):
    def __init__(self,parent,baseClass,includeBase=True):
        import woo.system
        QDialog.__init__(self,parent)
        self.setWindowTitle('Create new object of type %s'%baseClass.__name__)
        self.layout=QVBoxLayout(self)
        self.combo=QComboBox(self)
        self.classes=list(woo.system.childClasses(baseClass,includeBase=False)); self.classes.sort()
        if includeBase:
            self.classes=[baseClass,None]+self.classes # None is for the separator, so that indices are the same
            self.combo.addItem(baseClass.__name__)
            self.combo.insertSeparator(1000)
        self.combo.addItems([c.__name__ for c in self.classes[(2 if includeBase else 0):]])
        self.combo.currentIndexChanged.connect(self.comboSlot)
        self.scroll=QScrollArea(self)
        self.scroll.setWidgetResizable(True)
        self.buttons=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel);
        self.buttons.accepted.connect(self.accept)
        self.buttons.rejected.connect(self.reject)
        self.layout.addWidget(self.combo)
        self.layout.addWidget(self.scroll)
        self.layout.addWidget(self.buttons)
        self.ser=None
        self.combo.setCurrentIndex(0); self.comboSlot(0)
        self.setWindowModality(Qt.WindowModal)
    def comboSlot(self,index):
        self.ser=self.classes[index]() # instantiate the class
        self.scroll.setWidget(ObjectEditor(self.ser,self.scroll,showType=True))
        self.scroll.show()
    def result(self): return self.ser
    def sizeHint(self): return QSize(180,400)

class SeqFundamentalEditor(QFrame):
    # maximum length of the sequence; show ellipsis in the middle if longer
    # this should avoid freezes in the UI due to unexpectedly long data
    maxShowLen=60

    #def focusInEvent(self,event):
    #    log.warning('SeqFundamentalEditor: focusInEvent')

    def __init__(self,parent,getter,setter,itemType):
        QFrame.__init__(self,parent)
        self.getter,self.setter,self.itemType=getter,setter,itemType
        self.layout=QVBoxLayout()
        topLineFrame=QFrame(self); topLineLayout=QHBoxLayout(topLineFrame)
        self.form=QFormLayout()
        self.form.setContentsMargins(0,0,0,0)
        self.form.setVerticalSpacing(0)
        self.form.setLabelAlignment(Qt.AlignLeft)
        self.formFrame=QFrame(self); self.formFrame.setLayout(self.form)
        self.layout.addWidget(self.formFrame)
        self.setLayout(self.layout)
        self.emptySeq=False
        self.convSpec=None # cache value of unit conversions, for rows being added
        self.multiplier=None
        # ObjectEditor API compat
        self.hot=False
        self.rebuild()
        # periodic refresh
        self.refreshTimer=QTimer(self)
        self.refreshTimer.timeout.connect(self.refreshEvent)
        self.refreshTimer.start(500) # 1s should be enough
    def contextMenuEvent(self, event):
        # may return -1, which is OK
        index=self.localPositionToIndex(event.pos())
        seq=self.getter()
        if len(seq)==0: index=-1
        item=self.form.itemAt(index,QFormLayout.LabelRole)
        field=item.widget() if (index>=0 and item) else None
        menu=QMenu(self)
        actNew,actKill,actUp,actDown,actFromClip=[menu.addAction(name) for name in (u'☘ New',u'☠ Remove',u'↑ Up',u'↓ Down',u'↵ From clipboard')]
        if index<0: [a.setEnabled(False) for a in (actKill,actUp,actDown)]
        if index==len(seq)-1: actDown.setEnabled(False)
        if index==0: actUp.setEnabled(False)
        # disable until we figure out how to cancel when the no item in the menu is chosen
        #if field: field.setStyleSheet('QWidget { background: green }')

        menu.popup(self.mapToGlobal(event.pos()))
        actNew.triggered.connect(lambda: self.newSlot(index))
        actKill.triggered.connect(lambda: self.killSlot(index))
        actUp.triggered.connect(lambda: self.upSlot(index))
        actDown.triggered.connect(lambda: self.downSlot(index))
        actFromClip.triggered.connect(lambda: self.fromClipSlot(index))
        # this does not work...?!
        #menu.destroyed.connect(lambda: field.setStyleSheet('QWidget { background : none }'))
    def localPositionToIndex(self,pos,isGlobal=False):
        gp=self.mapToGlobal(pos) if not isGlobal else pos
        for row in range(self.form.rowCount()):
            w,i=self.form.itemAt(row,QFormLayout.FieldRole),self.form.itemAt(row,QFormLayout.LabelRole)
            for wi in w.widget(),i.widget():
                x0,y0,x1,y1=wi.geometry().getCoords(); globG=QRect(self.mapToGlobal(QPoint(x0,y0)),self.mapToGlobal(QPoint(x1,y1)))
                if globG.contains(gp):
                    return self.mapList2Seq(row)
        return -1
    def keyFocusIndex(self):
        w=QApplication.focusWidget()
        globPos=w.mapToGlobal(QPoint(.5*w.width(),.5*w.height()))
        return self.localPositionToIndex(globPos,isGlobal=True)
    def keyPressEvent(self,ev):
        isModified=ev.modifiers()&Qt.AltModifier
        if not isModified:
            if not QApplication.focusWidget(): return
            if ev.key()==Qt.Key_Return: QApplication.focusWidget().focusNextPrevChild(True)
            if ev.key()==Qt.Key_Up: QApplication.focusWidget().focusNextPrevChild(False)
            if ev.key()==Qt.Key_Down: QApplication.focusWidget().focusNextPrevChild(True)
            return
        if ev.key()==Qt.Key_Up:
            self.upSlot(self.keyFocusIndex()); ev.accept()
        elif ev.key()==Qt.Key_Down: self.downSlot(self.keyFocusIndex())
        elif ev.key()==Qt.Key_Delete or ev.key()==Qt.Key_Minus:
            self.killSlot(self.keyFocusIndex())
            ev.accept()
        elif ev.key()==Qt.Key_Backspace:
            self.killSlot(self.keyFocusIndex()-1)
            ev.accept()
        elif ev.key()==Qt.Key_Enter or ev.key()==Qt.Key_Return or ev.key()==Qt.Key_Plus:
            # insert after the current row
            self.newSlot(self.keyFocusIndex())
            ev.accept();
    def newSlot(self,i): # i is the index AFTER which the new row is inserted
        seq=self.getter();
        seq.insert(i+1,_fundamentalInitValues.get(self.itemType,self.itemType()))
        try: self.setter(seq)
        except BaseException as e: log.warning(f'Error setting new value {seq}: {e}')
        self.rebuild()
        if len(seq)>0:
            item=self.form.itemAt(i+1,QFormLayout.FieldRole)
            if item: item.widget().setFocus()
    def killSlot(self,i):
        seq=self.getter();
        if i<0: return
        assert(i<len(seq)); del seq[i]; self.setter(seq)
        self.refreshEvent()
        if len(seq)>0: self.form.itemAt(max(0,i if i<len(seq)-1 else i-1),QFormLayout.FieldRole).widget().setFocus()
    def upSlot(self,i):
        if i==0: return
        seq=self.getter(); assert(i<len(seq));
        prev,curr=seq[i-1:i+1];
        seq[i-1],seq[i]=curr,prev;
        self.setter(seq)
        self.refreshEvent(forceIx=i-1)
    def downSlot(self,i):
        seq=self.getter();
        if i==len(seq)-1: return
        assert(i<len(seq)-1);
        curr,nxt=seq[i:i+2]; seq[i],seq[i+1]=nxt,curr; self.setter(seq)
        self.refreshEvent(forceIx=i+1)
    def fromClipSlot(self,i):
        try:
            importables={Vector3:(float,3),Vector2:(float,2),Vector6:(float,6),Vector2i:(int,2),Vector2i:(int,3),Vector6i:(int,6),Matrix3:(float,9),float:(float,1),int:(int,1)}
            if self.itemType not in importables: raise NotImplementedError("Type %s is not text-importable"%(self.itemType.__name__))
            elementType,lineLen=importables[self.itemType]
            print('Will import lines with %d item(s) of type %s'%(lineLen,elementType.__name__))
            # get txt from clipboard
            cb=QApplication.clipboard()
            txt=str(cb.text())
            print('Got %d lines from clipboard:\n'%(len(txt.split('\n'))),txt)
            # handle unit conversions here
            if self.multiplier:
                if isinstance(self.multiplier,tuple): mult=[1./m for m in self.multiplier]
                else: mult=[1./self.multiplier]*lineLen
                print('Input will be scaled by',mult,' to match selected units')
            else: mult=[1.]*lineLen
            #
            seq=[]
            for i,ll in enumerate(txt.split('\n')):
                l=ll.split()
                if len(l)==0: continue # skip empty lines
                if len(l)!=lineLen: raise ValueError("Line %d has %d elements (should have %d)"%(i,len(l),lineLen))
                print('Line tuple is',tuple([val for val in l]))
                lineItems=[elementType(eval(val))*mult[i] for i,val in enumerate(l)]
                if lineLen>1: seq.append(tuple(lineItems))
                else: seq.append(lineItems[0]) # sequences of floats/ints are imported as sequence, not as sequence of tuples
            print('Imported sequence',seq)
        except Exception as e:
            import traceback
            traceback.print_exc()
            showExceptionDialog(self,e)
            return
        self.setter(seq)
        self.refreshEvent(forceIx=0)
    def mapList2Seq(self,l):
        'return sequence (object) index from given list (widget) index.'
        if not self.split: return l
        if l>self.split[0]: return l+self.split[1]-1  # 1 for the separator widget
    def mapSeq2List(self,s):
        'return list (widget) index from given sequence (object) index. Return -1 if the item is not shown in the UI due to splitting.'
        if not self.split: return s
        if s<self.split[0]: return s
        if s<self.split[1]: return -1
        if s>=self.split[1]: return s+(self.split[1]-self.split[0])+1 # 1 for the separator widget
    def recomputeSplit(self,currLen):
        l=currLen; ml=SeqFundamentalEditor.maxShowLen
        if l>ml:
            self.split=(ml/2,l-ml/2)
            self.splitLen=l
        else:
            self.split=None
            self.splitLen=0

    def renumber(self,currLen):
        if not self.split: return
        self.recomputeSplit()
        for row in range(self.form.rowCount()):
            l=self.form.itemAt(row,QFormLayout.LabelRole)
            l.widget().setText('%d. '%self.mapList2Seq(row))
        self.splitLen=currLen

    def rebuild(self):
        log.debug('rebuild...')
        currSeq=self.getter()
        # clear everything
        for row in range(self.form.rowCount()):
            log.debug(f'counts {self.form.rowCount()} {self.form.count()}')
            for wi in self.form.itemAt(row,QFormLayout.FieldRole),self.form.itemAt(row,QFormLayout.LabelRole):
                self.form.removeItem(wi)
                if not wi or not wi.widget(): continue
                wi.focusOutEvent=None
                log.trace('deleting widget {wi.widget()}')
                # for some reason, deleting does not make the thing disappear visually; hiding does, however
                # FIXME: this might be the reason why ever-resizing sequences eat up RAM!!
                widget=wi.widget(); widget.hide(); del widget 
            log.trace(f'counts after {self.form.rowCount()} {self.form.count()}')
        while self.form.rowCount()>0: self.form.removeRow(0)
        #for row in range(self.form.rowCount()): row.
        log.debug('cleared')
        # add everything
        Klass=_fundamentalEditorMap.get(self.itemType,None)
        if not Klass:
            errMsg=QTextEdit(self)
            errMsg.setReadOnly(True); errMsg.setText("Sorry, editing sequences of %s's is not (yet?) implemented."%(self.itemType.__name__))
            self.form.insertRow(0,'<b>Error</b>',errMsg)
            return
        class ItemGetter():
            def __init__(self,getter,index): self.getter,self.index=getter,index
            def __call__(self):
                try:
                    return self.getter()[self.index]
                except IndexError: return None
        class ItemSetter():
            def __init__(self,getter,setter,index): self.getter,self.setter,self.index=getter,setter,index
            def __call__(self,val):
                try:
                    seq=self.getter(); seq[self.index]=val; self.setter(seq)
                except IndexError: pass

        self.recomputeSplit(len(currSeq))

        for i,item in enumerate(currSeq):
            if self.split and self.split[0]==i:
                # show separator
                l=QLabel(u'<b>⋮</b>'); l.font().setPointSize(32)
                self.form.insertRow(i,u'<b>⋮</b>',l)
                continue 
            li=self.mapSeq2List(i)
            # print 'item #%d mapped to field %d'%(i,li)
            if li<0: continue # in the split, do not show anything
            widget=Klass(self,ItemGetter(self.getter,i),ItemSetter(self.getter,self.setter,i))
            # self.hijackWidgetEvents(widget)
            self.form.insertRow(i,'%d. '%li,widget)
            log.debug('added item %d %s'%(i,str(widget)))
            # set units correctly
            if self.multiplier:
                widget.multiplier=self.multiplier
                widget.multiplierChanged(self.convSpec)
        if len(currSeq)==0:
            self.emptySeq=True
            self.form.insertRow(0,'<i>empty</i>',QLabel('<i>(right-click for menu)</i>'))
        else: self.emptySeq=False
        log.debug('rebuilt, will refresh now')
        self.refreshEvent(dontRebuild=True) # avoid infinite recursion it the length would change meanwhile

    def refreshEvent(self,dontRebuild=False,forceIx=-1):
        # log.warning('refreshEvent...')
        currSeq=self.getter()
        #print 'bbb',len(currSeq)
        if not self.split and (len(currSeq)!=(self.form.rowCount() if not self.emptySeq else 0)):
            if dontRebuild: return # length changed behind our back, just pretend nothing happened and update next time instead
            log.warning(f'Sequence length not matching (data: {len(currSeq)}, form: {self.form.rowCount()}), rebuilding')
            self.rebuild()
            currSeq=self.getter()
        elif self.split and len(currSeq)!=self.splitLen: self.renumber(len(currSeq))
        for i in range(len(currSeq)):
            item=self.form.itemAt(i,QFormLayout.FieldRole)
            if not item: continue # some error condition, oh well
            widget=item.widget()
            log.trace('got item #%d %s'%(i,str(widget)))
            if hasattr(widget,'hot') and not widget.hot: # it can be a QLabel as well
                widget.refresh()
            if forceIx>=0 and forceIx==i: widget.setFocus()
    def refresh(self): pass # ObjectEditor API
    # propagate multiplier change to children
    def multiplierChanged(self,convSpec):
        self.convSpec=convSpec # cache value should new rows be created
        self.setToolTip(convSpec)
        for row in range(self.form.count()//2):
            w=self.form.itemAt(row,QFormLayout.FieldRole).widget()
            if isinstance(w,QLabel): continue # label that the sequence is empty
            w.multiplier=self.multiplier
            w.multiplierChanged(convSpec)
    




